/**
 * Copyright (C) 2016-2019 DSpot Sp. z o.o
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dspot.declex.handler.base;

import static com.dspot.declex.api.util.FormatsUtils.fieldToGetter;
import static com.helger.jcodemodel.JExpr._super;
import static com.helger.jcodemodel.JExpr.invoke;
import static com.helger.jcodemodel.JExpr.ref;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

import org.ohosannotations.OhosAnnotationsEnvironment;
import org.ohosannotations.ElementValidation;
import org.ohosannotations.handler.BaseAnnotationHandler;
import org.ohosannotations.holder.EComponentHolder;
import org.ohosannotations.holder.EComponentWithViewSupportHolder;
import org.ohosannotations.logger.Logger;
import org.ohosannotations.logger.LoggerFactory;

import org.ohosannotations.annotations.export.Exported;

import com.dspot.declex.annotation.RunWith;
import com.dspot.declex.helper.EventsHelper;
import com.dspot.declex.holder.ViewsHolder;
import com.dspot.declex.holder.ViewsHolder.WriteInBlockWithResult;
import com.dspot.declex.holder.view_listener.IStatementCreator;
import com.dspot.declex.holder.view_listener.ViewListenerHolder;
import com.dspot.declex.util.SharedRecords;
import com.dspot.declex.util.TypeUtils;

import org.ohosannotations.internal.virtual.VirtualElement;

import com.helger.jcodemodel.AbstractJClass;
import com.helger.jcodemodel.AbstractJType;
import com.helger.jcodemodel.IJExpression;
import com.helger.jcodemodel.IJStatement;
import com.helger.jcodemodel.JBlock;
import com.helger.jcodemodel.JFieldRef;
import com.helger.jcodemodel.JInvocation;
import com.helger.jcodemodel.JMethod;
import com.helger.jcodemodel.JMod;

/**
 * 基本事件处理程序
 *
 * @author Administrator
 * @date 2021/07/27
 */
public abstract class BaseEventHandler<T extends EComponentHolder> extends BaseAnnotationHandler<T> {

    /**
     * 日志记录器
     */
    protected static final Logger LOGGER = LoggerFactory.getLogger(BaseEventHandler.class);

    /**
     * 引用id
     */
    private String referencedId;

    /**
     * 事件的助手
     */
    private EventsHelper eventsHelper;

    /**
     * 基本事件处理程序
     *
     * @param targetClass 目标类
     * @param environment 环境
     */
    public BaseEventHandler(Class<?> targetClass, OhosAnnotationsEnvironment environment) {
        super(targetClass, environment);
        eventsHelper = EventsHelper.getInstance(environment);
    }

    /**
     * 验证
     *
     * @param element 元素
     * @param valid 有效的
     */
    @Override
    public void validate(Element element, ElementValidation valid) {

        validatorHelper.enclosingElementHasEnhancedComponentAnnotation(element, valid);

    }

    /**
     * 得到的名字
     *
     * @param element 元素
     * @return {@link List<String>}
     */
    protected List<String> getNames(Element element) {
        return Arrays.asList(element.getSimpleName().toString());
    }

    /**
     * 让听众持有人
     *
     * @param elementName 元素名称
     * @param elementClass 元素类
     * @param declForListener decl为侦听器
     * @param element 元素
     * @param viewsHolder 观点持有者
     * @param holder 持有人
     * @return {@link ViewListenerHolder}
     */
    protected ViewListenerHolder getListenerHolder(String elementName, String elementClass, Map<AbstractJClass, IJExpression> declForListener,
                                                   Element element, ViewsHolder viewsHolder, T holder) {

        //Check if it is an event, only permitted in fields
        if (element.getKind().equals(ElementKind.FIELD) && referencedId.startsWith("on")) {
            String eventClassName = SharedRecords.getEvent(referencedId.substring(2), getEnvironment());

            if (eventClassName != null) {
                JBlock eventBlock = eventsHelper.addEventListener(eventClassName, element.getEnclosingElement(), viewsHolder);

                IJStatement statement = getStatement(getJClass(eventClassName), element, viewsHolder, holder);
                if (statement != null) {
                    eventBlock.add(statement);
                }

                return null;
            }
        }

        //Fallback as Method call
        methodHandler(elementClass, referencedId, element, viewsHolder, holder);
        return null;
    }

    /**
     * 得到类名
     *
     * @param element 元素
     * @return {@link String}
     */
    protected String getClassName(Element element) {
        return TypeUtils.getGeneratedClassName(element, getEnvironment());
    }

    /**
     * 过程
     *
     * @param element 元素
     * @param holder 持有人
     */
    @Override
    public void process(Element element, final T holder) {

        ViewsHolder viewsHolder = null;
        if (holder instanceof EComponentWithViewSupportHolder) {
            viewsHolder = holder.getPluginHolder(new ViewsHolder((EComponentWithViewSupportHolder) holder));
        }

        final Map<AbstractJClass, IJExpression> declForListener = new HashMap<>();
        final String elementClass = getClassName(element);

        for (final String elementName : getNames(element)) {

            referencedId = elementName;

            final ViewListenerHolder listenerHolder = getListenerHolder(elementName, elementClass, declForListener, element, viewsHolder, holder);
            if (listenerHolder == null) continue;

            if (viewsHolder != null) {
                for (Map.Entry<AbstractJClass, IJExpression> entry : declForListener.entrySet()) {
                    listenerHolder.addDecl(referencedId, JMod.FINAL, entry.getKey(), "model", declForListener.get(entry.getKey()));
                }

                listenerHolder.addStatement(
                    referencedId,
                    new StatementCreator(elementClass == null ? null : getJClass(elementClass), element, viewsHolder, holder)
                );

                //If it's found the the class associated layout, then process the event here
                if (viewsHolder.layoutContainsId(referencedId)) {
                    viewsHolder.createAndAssignView(referencedId, new WriteInBlockWithResult<JBlock>() {

                        @Override
                        public void writeInBlock(String viewName, AbstractJClass viewClass, JFieldRef view, JBlock block) {
                            listenerHolder.createListener(viewName, block);
                        }
                    });
                }
            }

        }
    }

    /**
     * 方法处理程序
     *
     * @param elementClass 元素类
     * @param elementName 元素名称
     * @param element 元素
     * @param viewsHolder 观点持有者
     * @param holder 持有人
     * @return boolean
     */
    private boolean methodHandler(String elementClass, String elementName, Element element, ViewsHolder viewsHolder, T holder) {

        //See if the method exists in the holder
        final String methodName = fieldToGetter(elementName);

        //Try to find the method using reflection
        //TODO This search by method name in the holder should be improve, one approach is to
        //search in methods of the holder annotated by some annotation which provides the method which
        //it references
        Method holderMethod = null;
        try {
            holderMethod = holder.getClass().getMethod(methodName);
        } catch (NoSuchMethodException | SecurityException e) {
            try {
                holderMethod = holder.getClass().getMethod(methodName + "Method");
            } catch (NoSuchMethodException | SecurityException e1) {
                try {
                    holderMethod = holder.getClass().getMethod(methodName + "Body");
                } catch (NoSuchMethodException | SecurityException e2) {
                    try {
                        holderMethod = holder.getClass().getMethod(methodName + "AfterSuperBlock");
                    } catch (NoSuchMethodException | SecurityException e3) {
                        try {
                            holderMethod = holder.getClass().getMethod(methodName + "BeforeSuperBlock");
                        } catch (NoSuchMethodException | SecurityException e4) {

                        }
                    }
                }
            }
        }

        if (holderMethod != null) {
            try {
                Object result = holderMethod.invoke(holder);

                JBlock block = null;
                if (result instanceof JMethod) {
                    block = ((JMethod) result).body();
                } else if (result instanceof JBlock) {
                    block = (JBlock) result;
                }

                if (block != null) {

                    if (element.getAnnotation(RunWith.class) != null && adiHelper.getAnnotation(element, Exported.class) != null) {

                        //If this is an @Exported @RunWith method, it should not be call by itself,
                        //this code avoids this issue (an infinite loop)

                        //This will be called by ExernalHandler as well
                        JMethod method = codeModelHelper.overrideAnnotatedMethod((ExecutableElement) element, holder, false, false);
                        if (method == result) return true;
                    }

                    IJStatement statement = getStatement(getJClass(elementClass), element, viewsHolder, holder);
                    if (statement != null) {
                        block.add(statement);
                    }
                    return true;
                }
            } catch (IllegalAccessException | IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                System.out.println("Detected with: " + holderMethod.getName() + " in " + holder.getClass());
                e.getCause().printStackTrace();
            }
        }

        if (element instanceof ExecutableElement) {

            //This give support to Override methods where
            if (element.getAnnotation(RunWith.class) != null) {

                if (elementName.endsWith("_")) {
                    elementName = elementName.substring(0, elementName.length() - 1);
                    return methodHandler(elementClass, elementName, element, viewsHolder, holder);
                }

                //Override methods is handled in RunWithHandler
                if (element.getAnnotation(Override.class) != null) {
                    return true;
                }

                //Navigate in super parent looking for coinciding methods
                ExecutableElement executableElement = (ExecutableElement) element;

                //Look for similar methods in super classes
                List<? extends TypeMirror> superTypes =
                    getProcessingEnvironment().getTypeUtils().directSupertypes(element.getEnclosingElement().asType());

                for (TypeMirror type : superTypes) {
                    TypeElement superElement = getProcessingEnvironment().getElementUtils().getTypeElement(type.toString());
                    if (superElement == null) continue;

                    if (foundMethodIn(superElement, executableElement, elementName)) {

                        final TypeMirror resultType = executableElement.getReturnType();
                        final List<? extends VariableElement> parameters = executableElement.getParameters();

                        final List<AbstractJType> parametersType = new ArrayList<>(parameters.size());
                        for (VariableElement param : parameters) {
                            parametersType.add(codeModelHelper.elementTypeToJClass(param));
                        }

                        JMethod method = holder.getGeneratedClass().getMethod(
                            elementName, parametersType.toArray(new AbstractJType[parameters.size()]));

                        if (method == null) {
                            method = holder.getGeneratedClass().method(
                                JMod.PUBLIC, codeModelHelper.elementTypeToJClass(executableElement), elementName);
                            method.annotate(Override.class);

                            for (VariableElement param : parameters) {
                                method.param(codeModelHelper.elementTypeToJClass(param), param.getSimpleName().toString());
                            }
                        }

                        if (adiHelper.getAnnotation(element, Exported.class) != null) {

                            //If this is an @Exported @RunWith method, it should not be called by itself,
                            //this code avoids this issue (an infinite loop)

                            //This will be called by ExernalHandler as well
                            JMethod externalMethod = codeModelHelper.overrideAnnotatedMethod((ExecutableElement) element, holder, false, false);
                            if (method == externalMethod) return true;
                        }

                        method.body().add(getStatement(getJClass(elementClass), element, viewsHolder, holder));

                        return true;
                    }
                }

            }

            //The same method invocating itself is handled in a different
            //handler (Ex. RunWithHandler)
            return true;
        }

        //Search coinciding methods
        //TODO recursive search in all the methods of the super classes
        List<? extends Element> elems = element.getEnclosingElement().getEnclosedElements();
        List<Element> allElems = new LinkedList<>(elems);
        allElems.addAll(VirtualElement.getVirtualEnclosedElements(element.getEnclosingElement()));

        for (Element elem : allElems)
            if (elem.getKind() == ElementKind.METHOD) {
                if (elem.getModifiers().contains(Modifier.PRIVATE) ||
                    elem.getModifiers().contains(Modifier.STATIC)) {
                    continue;
                }

                if (elem.getSimpleName().toString().equals(elementName)) {
                    ExecutableElement executableElem = (ExecutableElement) elem;

                    List<? extends VariableElement> parameters = executableElem.getParameters();
                    TypeMirror resultType = executableElem.getReturnType();

                    JMethod method = holder.getGeneratedClass().method(
                        JMod.PUBLIC, getJClass(resultType.toString()), elementName
                    );
                    method.body().add(getStatement(getJClass(elementClass), element, viewsHolder, holder));

                    JInvocation superInvoke = invoke(_super(), elementName);
                    for (VariableElement param : parameters) {
                        method.param(codeModelHelper.elementTypeToJClass(param), param.getSimpleName().toString());
                        superInvoke = superInvoke.arg(ref(param.getSimpleName().toString()));
                    }

                    if (!executableElem.getModifiers().contains(Modifier.ABSTRACT)) {
                        if (resultType.getKind().equals(TypeKind.VOID)) {
                            method.body().add(superInvoke);
                        } else {
                            method.body()._return(superInvoke);
                        }
                    }

                    return true;
                }

            }

        return false;
    }

    /**
     * 发现方法
     *
     * @param element 元素
     * @param executableElement 可执行元素
     * @param elementName 元素名称
     * @return boolean
     */
    private boolean foundMethodIn(Element element, ExecutableElement executableElement, String elementName) {
        List<? extends Element> elems = element.getEnclosedElements();
        List<Element> allElems = new LinkedList<>(elems);
        allElems.addAll(VirtualElement.getVirtualEnclosedElements(element));

        ELEMENTS:
        for (Element elem : allElems) {
            if (elem.getKind() == ElementKind.METHOD) {
                if (elem.getModifiers().contains(Modifier.PRIVATE) ||
                    elem.getModifiers().contains(Modifier.STATIC)) {
                    continue;
                }

                if (elem.getSimpleName().toString().equals(elementName)) {
                    ExecutableElement executableElem = (ExecutableElement) elem;

                    TypeMirror resultType = executableElem.getReturnType();
                    if (!TypeUtils.isSubtype(executableElement.getReturnType(), resultType, getProcessingEnvironment()))
                        continue;

                    List<? extends VariableElement> parameters = executableElem.getParameters();
                    if (parameters.size() != executableElement.getParameters().size()) continue;

                    for (int i = 0; i < parameters.size(); i++) {
                        if (!parameters.get(i).asType().toString().equals(
                            executableElement.getParameters().get(i).asType().toString()
                        )) continue ELEMENTS;
                    }

                    //Method was found
                    return true;
                }

            }
        }

        List<? extends TypeMirror> superTypes = getProcessingEnvironment().getTypeUtils().directSupertypes(element.asType());
        for (TypeMirror type : superTypes) {
            TypeElement superElement = getProcessingEnvironment().getElementUtils().getTypeElement(type.toString());
            if (superElement == null) continue;
            if (foundMethodIn(superElement, executableElement, elementName)) return true;
        }

        return false;
    }

    /**
     * 得到的声明
     *
     * @param elementClass 元素类
     * @param element 元素
     * @param viewsHolder 观点持有者
     * @param holder 持有人
     * @return {@link IJStatement}
     */
    protected abstract IJStatement getStatement(AbstractJClass elementClass, Element element, ViewsHolder viewsHolder, T holder);

    protected class StatementCreator implements IStatementCreator {

        AbstractJClass elementClass;
        Element element;
        ViewsHolder viewsHolder;
        T holder;

        public StatementCreator(AbstractJClass elementClass, Element element,
                                ViewsHolder viewsHolder, T holder) {
            this.elementClass = elementClass;
            this.element = element;
            this.viewsHolder = viewsHolder;
            this.holder = holder;
        }

        @Override
        public IJStatement getStatement() {
            return BaseEventHandler.this.getStatement(elementClass, element, viewsHolder, holder);
        }

    }
}
